/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    Array.java
 *    Copyright (C) 2009-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core.pmml;

import java.io.Serializable;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import org.w3c.dom.Element;

/**
 * Class for encapsulating a PMML Array element.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision$
 */
public class Array implements Serializable {

    /** ID added to avoid warning */
    private static final long serialVersionUID = 4286234448957826177L;

    /**
     * Utility method to check if an XML element is an array.
     * 
     * @param arrayE the XML element to check
     * @return returns true if the XML element is an array
     */
    public static boolean isArray(Element arrayE) {
        String name = arrayE.getTagName();

        if (name.equals("Array") || name.equals("NUM-ARRAY") || name.equals("INT-ARRAY") || name.equals("REAL-ARRAY") || name.equals("STRING-ARRAY") || isSparseArray(arrayE)) {
            return true;
        }
        return false;
    }

    /**
     * Utility method to check if an XML element is a sparse array.
     * 
     * @param arrayE the XML element to check.
     * @return true if the XML element is a sparse array.
     */
    private static boolean isSparseArray(Element arrayE) {
        String name = arrayE.getTagName();

        if (name.equals("INT-SparseArray") || name.equals("REAL-SparseArray")) {
            return true;
        }

        return false;
    }

    public static Array create(List<Object> values, List<Integer> indices) throws Exception {

        ArrayType type = null;

        Object first = values.get(0);
        if ((first instanceof Double) || (first instanceof Float)) {
            type = ArrayType.REAL;
        } else if ((first instanceof Integer) || (first instanceof Long)) {
            type = ArrayType.INT;
        } else if ((first instanceof String)) {
            type = ArrayType.STRING;
        } else {
            throw new Exception("[Array] unsupport type!");
        }

        if (indices != null) {
            // array is sparse

            if (indices.size() != values.size()) {
                throw new Exception("[Array] num values is not equal to num indices!!");
            }

            if (type == ArrayType.REAL) {
                type = ArrayType.REAL_SPARSE;
            } else if (type == ArrayType.INT) {
                type = ArrayType.INT_SPARSE;
            } else {
                throw new Exception("[Array] sparse arrays can only be integer, long, float or double!");
            }

            return new SparseArray(type, values, indices);
        }

        return new Array(type, values);
    }

    /**
     * Static factory method for creating non-sparse or sparse array types as
     * needed.
     * 
     * @param arrayE the XML element encapsulating the array
     * @return an appropriate Array type
     * @throws Exception if there is a problem when constructing the array
     */
    public static Array create(Element arrayE) throws Exception {
        if (!isArray(arrayE)) {
            throw new Exception("[Array] the supplied element does not contain an array!");
        }

        if (isSparseArray(arrayE)) {
            return new SparseArray(arrayE);
        }

        return new Array(arrayE);
    }

    public static enum ArrayType {
        NUM("NUM-ARRAY"), INT("INT-ARRAY"), REAL("REAL-ARRAY"), STRING("STRING-ARRAY"), REAL_SPARSE("REAL-SparseArray"), INT_SPARSE("INT-SparseArray");

        private final String m_stringVal;

        ArrayType(String name) {
            m_stringVal = name;
        }

        @Override
        public String toString() {
            return m_stringVal;
        }
    }

    /** The values of the array */
    protected ArrayList<String> m_values = new ArrayList<String>();

    /** The type of the array */
    protected ArrayType m_type = ArrayType.NUM;

    protected void initialize(Element arrayE) throws Exception {
        String arrayS = arrayE.getTagName();

        // get the type of the array
        if (arrayS.equals("Array")) {
            String type = arrayE.getAttribute("type");
            if (type.equals("int")) {
                m_type = ArrayType.INT;
            } else if (type.equals("real")) {
                m_type = ArrayType.REAL;
            } else if (type.equals("string")) {
                m_type = ArrayType.STRING;
            }
        } else {
            for (ArrayType a : ArrayType.values()) {
                if (a.toString().equals(arrayS)) {
                    m_type = a;
                    break;
                }
            }
        }
        // now read the values
        String contents = arrayE.getChildNodes().item(0).getNodeValue();
        StringReader sr = new StringReader(contents);
        StreamTokenizer st = new StreamTokenizer(sr);
        st.resetSyntax();
        st.whitespaceChars(0, ' ');
        st.wordChars(' ' + 1, '\u00FF');
        st.whitespaceChars(' ', ' ');
        st.quoteChar('"');
        st.quoteChar('\'');
        // m_Tokenizer.eolIsSignificant(true);

        st.nextToken();
        while (st.ttype != StreamTokenizer.TT_EOF && st.ttype != StreamTokenizer.TT_EOL) {
            m_values.add(st.sval);
            st.nextToken();
        }
    }

    /**
     * Construct an array from an XML node
     * 
     * @param arrayE the Element containing the XML
     * @throws Exception if something goes wrong
     */
    protected Array(Element arrayE) throws Exception {
        initialize(arrayE);
    }

    /**
     * Construct an array from the given values.
     * 
     * @param type   the type of the elements.
     * @param values the values of the array.
     */
    protected Array(ArrayType type, List<Object> values) {
        m_values = new ArrayList<String>();
        m_type = type;

        for (Object o : values) {
            m_values.add(o.toString());
        }
    }

    /**
     * Get the type of this array.
     * 
     * @return the type of the array.
     */
    public ArrayType getType() {
        return m_type;
    }

    /**
     * Is this array a SparseArray?
     * 
     * @return true if this array is sparse.
     */
    public boolean isSparse() {
        return false;
    }

    /**
     * Get the number of values in this array.
     * 
     * @return the number of values in this array.
     */
    public int numValues() {
        return m_values.size();
    }

    /**
     * Returns true if the array contains this string value.
     * 
     * @param value the value to check for.
     * @return true if the array contains this string value
     */
    public boolean contains(String value) {
        return m_values.contains(value);
    }

    /**
     * Returns true if the array contains this integer value.
     * 
     * @param value the value to check for
     * @return true if the array contains this integer value
     */
    public boolean contains(int value) {
        return contains(new Integer(value).toString());
    }

    /**
     * Returns true if the array contains this real value.
     * 
     * @param value the value to check for
     * @return true if the array contains this real value
     */
    public boolean contains(double value) {
        return contains(new Double(value).toString());
    }

    /**
     * Returns true if the array contains this real value.
     * 
     * @param value the value to check for
     * @return true if the array contains this real value
     */
    public boolean contains(float value) {
        return contains(new Float(value).toString());
    }

    private void checkInRange(int index) throws Exception {
        if (index >= m_values.size() || index < 0) {
            throw new IllegalArgumentException("[Array] index out of range " + index);
        }
    }

    /**
     * Returns the index of the value stored at the given position
     * 
     * @param position the position
     * @return the index of the value stored at the given position
     */
    public int index(int position) {
        return position; // position is the index for dense arrays
    }

    /**
     * Gets the value at index from the array.
     * 
     * @param index the index of the value to get.
     * @return the value at index in the array as as String.
     * @throws Exception if index is out of bounds.
     */
    public String value(int index) throws Exception {
        return actualValue(index);
    }

    /**
     * Gets the value at index from the array
     * 
     * @param index the index of the value to get.
     * @return the value at index in the array as as String.
     * @throws Exception if index is out of bounds.
     */
    protected String actualValue(int index) throws Exception {
        checkInRange(index);
        return m_values.get(index);
    }

    /**
     * Gets the value at index from the array as a String. Calls value().
     * 
     * @param index the index of the value to get.
     * @return the value at index in the array as a String.
     * @throws Exception if index is out of bounds.
     */
    public String valueString(int index) throws Exception {
        return value(index);
    }

    /**
     * Gets the value at index from the array as a double.
     * 
     * @param index the index of the value to get.
     * @return the value at index in the array as a double.
     * @throws Exception if index is out of bounds.
     */
    public double valueDouble(int index) throws Exception {
        if (m_type == ArrayType.STRING) {
            throw new Exception("[Array] Array does not contain numbers!");
        }
        return Double.parseDouble(value(index));
    }

    /**
     * Gets the value at index from the array as a float.
     * 
     * @param index the index of the value to get.
     * @return the value at index in the array as a float.
     * @throws Exception if index is out of bounds.
     */
    public float valueFloat(int index) throws Exception {
        if (m_type == ArrayType.STRING) {
            throw new Exception("[Array] Array does not contain numbers!");
        }
        return Float.parseFloat(value(index));
    }

    /**
     * Gets the value at index from the array as an int.
     * 
     * @param index the index of the value to get.
     * @return the value at index in the array as an int.
     * @throws Exception if index is out of bounds.
     */
    public int valueInt(int index) throws Exception {
        if (m_type != ArrayType.INT && m_type != ArrayType.INT_SPARSE) {
            throw new Exception("[Array] Array does not contain integers!");
        }
        return Integer.parseInt(value(index));
    }

    /**
     * Gets the value at indexOfIndex from the array. Does the same as value() if
     * this array is not sparse.
     * 
     * @param indexOfIndex the index of the index of the value to get.
     * @return a value from the array as a String.
     * @throws Exception if indexOfIndex is out of bounds.
     */
    public String valueSparse(int indexOfIndex) throws Exception {
        return actualValue(indexOfIndex);
    }

    /**
     * Gets the value at indexOfIndex from the array. Does the same as value() if
     * this array is not sparse.
     * 
     * @param indexOfIndex the index of the index of the value to get.
     * @return a value from the array as a String.
     * @throws Exception if indexOfIndex is out of bounds.
     */
    public String valueSparseString(int indexOfIndex) throws Exception {
        return valueSparse(indexOfIndex);
    }

    /**
     * Gets the value at indexOfIndex from the array. Does the same as value() if
     * this array is not sparse.
     * 
     * @param indexOfIndex the index of the index of the value to get.
     * @return a value from the array as a double.
     * @throws Exception if indexOfIndex is out of bounds.
     */
    public double valueSparseDouble(int indexOfIndex) throws Exception {
        return Double.parseDouble(actualValue(indexOfIndex));
    }

    /**
     * Gets the value at indexOfIndex from the array. Does the same as value() if
     * this array is not sparse.
     * 
     * @param indexOfIndex the index of the index of the value to get.
     * @return a value from the array as a float.
     * @throws Exception if indexOfIndex is out of bounds.
     */
    public float valueSparseFloat(int indexOfIndex) throws Exception {
        return Float.parseFloat(actualValue(indexOfIndex));
    }

    /**
     * Gets the value at indexOfIndex from the array. Does the same as value() if
     * this array is not sparse.
     * 
     * @param indexOfIndex the index of the index of the value to get.
     * @return a value from the array as an int.
     * @throws Exception if indexOfIndex is out of bounds.
     */
    public int valueSparseInt(int indexOfIndex) throws Exception {
        return Integer.parseInt(actualValue(indexOfIndex));
    }

    @Override
    public String toString() {
        StringBuffer text = new StringBuffer();

        text.append("<");
        for (int i = 0; i < m_values.size(); i++) {
            text.append(m_values.get(i));
            if (i < m_values.size() - 1) {
                text.append(",");
            }
        }

        text.append(">");
        return text.toString();
    }
}
